/*  
 * Copyright 2007 MBARI 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
package org.mbari.timeline.piccolo;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.geom.Point2D;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.domainlanguage.base.Ratio;
import com.domainlanguage.base.Rounding;
import com.domainlanguage.time.Duration;
import com.domainlanguage.time.TimeInterval;
import com.domainlanguage.time.TimePoint;

import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.nodes.PPath;

/**
 * This node represents a continuous time series. It was originally designed to
 * display tide records but has been generalized to display any time series
 * date.
 * 
 * NOTE: This classes mixes presentation and model together. That is, most everything
 * in this class represents the model, except for the getTimeSeriesNode, which supplies
 * the graphical representation of the model
 * 
 * @author Andrew Chase
 * 
 */
public class TimeSeries {

	enum TideRecordParts {
		utcDate(1), utcTime(2), yValue(3);

		final public int index;

		private TideRecordParts(int index) {
			this.index = index;
		}
	}

	private List<Float> yValues;
	private TimePoint timeOfFirstRecord;
	private Duration timeBetweenRecords;
	private Pattern extractionPattern;
	private float verticalExageration = 25.0f;
	private String units = "";
	private Paint color = Color.red;

	/**
	 * The tide file
	 * 
	 * @param in
	 *            The file that contains the input data.
	 * @param extractionPattern
	 *            This pattern must match: group 1 to a UTCDate value in the
	 *            format "YYYY-(M)M-(D)D". group 2 to UTCTime value in the format
	 *            "HH:MM:SS". group 3 to the y value itself.
	 * 
	 */
	public TimeSeries(BufferedReader in, Pattern extractionPattern) {
		this.extractionPattern = extractionPattern;
		replaceDataSource(in);
	}

	/**
	 * This is a convenience method for working with data produced by the
	 * jtides.jar gui. All data files are expected to have a header line
	 * followed by data records. If you use jtides.jar to create tide file it
	 * will come in this format. The line format is as follows: UTCDate UTCTime
	 * Date Time Tide Units 2007-1-1 08:00:00 2007-1-1 00:00:00 +0.82 Meters
	 * 
	 * That is the format expected by this method
	 * 
	 * @param in
	 *            The input tides file
	 * @param valueDelimiter
	 *            The delimiter used in creating the tides file (eg "\\t" or
	 *            ",")
	 * @return
	 */
	public static TimeSeries makeTideNode(BufferedReader in,
			String valueDelimiter) {
		String tidePattern = "(.*?)" + valueDelimiter + "(.*?)"
				+ valueDelimiter + ".*?" + valueDelimiter + ".*?"
				+ valueDelimiter + "(.*?)" + valueDelimiter + ".*";
		Pattern extractionPattern = Pattern.compile(tidePattern);
		TimeSeries tide = new TimeSeries(in, extractionPattern);
		tide.setColor(Color.blue);
		tide.setUnits("meters");
		return tide;
	}

	private void setUnits(String string) {
		units = string;
	}

	public void replaceDataSource(BufferedReader reader) {
		String line;
		try {
			yValues = new ArrayList<Float>();
			// first line in the tide file will be header information
			reader.readLine();
			// mark this position so we can come back to it
			reader.mark(500);
			// read the first line to extract the firstTideRecordTime
			timeOfFirstRecord = parseTimeFromRecord(reader.readLine());
			TimePoint secondTideRecordTime = parseTimeFromRecord(reader
					.readLine());
			timeBetweenRecords = timeOfFirstRecord.until(secondTideRecordTime)
					.length();
			// okay, we've collected our metadata, let's move back to the mark
			// point and
			// start parsing values in earnest.
			reader.reset();
			while ((line = reader.readLine()) != null) {
				Matcher match = matchLine(line);
				float currentTide = Float.parseFloat(match
						.group(TideRecordParts.yValue.index));
				yValues.add(currentTide);
			}
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	private Matcher matchLine(String line) {
		Matcher match = extractionPattern.matcher(line);
		if (!match.matches()) {
			System.err.println("Skipping bad line in input file:");
			System.err.println(line);
		}
		return match;
	}

	// extract the UTC time from a given line
	private TimePoint parseTimeFromRecord(String line) {
		Matcher match = matchLine(line);
		String date = match.group(TideRecordParts.utcDate.index);
		String year = date.substring(0, 4);
		int endOfMonth = date.indexOf('-', 5);
		String month = date.substring(5, endOfMonth);
		if (month.length() == 1) {
			month = "0" + month;
		}
		String day = date.substring(endOfMonth + 1);
		if (day.length() == 1) {
			day = "0" + day;
		}
		String dateTime = year + month + day
				+ match.group(TideRecordParts.utcTime.index);
		return TimePoint.parseGMTFrom(dateTime, "yyyyMMddkk:mm:ss");
	}

	public PPath getTimeSeriesNode(TimePoint start, TimePoint end, final Timeline timeline) {
		TimeInterval requestedInterval = start.until(end);
		final int numRecords = (int) requestedInterval.length().dividedBy(
				timeBetweenRecords).decimalValue(1, Rounding.CEILING)
				.longValue();
		final int indexOffset = getOffsetIntoArray(start);
		if ((indexOffset + numRecords) > yValues.size()) {
			throw new IllegalArgumentException(
					"End time point requested goes beyond time range");
		}
		float[] xs = new float[numRecords];
		float[] ys = new float[numRecords];
		float xPos = 0;
		float xIncrement = (float) (timeline.getWidth() / numRecords);
		for (int i = 0; i < numRecords; i++) {
			float yValue = yValues.get(indexOffset + i);
			xPos += xIncrement;
			xs[i] = xPos;
			// multiply by negative one becuase java2d y coordinates are
			// reversed (0,0 is upper left corner)
			ys[i] = yValue * verticalExageration * -1;
		}
		final PPath timePath = PPath.createPolyline(xs, ys);
		timePath.setStroke(new BasicStroke(2.0f));
		timePath.setStrokePaint(color);
		timePath.addInputEventListener(new PBasicInputEventHandler() {
			public void mouseClicked(PInputEvent event) {
				Point2D positionOnTimeline = event
						.getPositionRelativeTo(timeline);
				TimePoint timeAtEvent = timeline
						.xCoordinateToDate(positionOnTimeline.getX());
				float tideHeight = getClosestValueToTime(timeAtEvent);
				System.out.println("time: " + timeAtEvent);
				System.out.println("tide height: " + tideHeight);
			}
		});
		Color transparent = new Color(0.0f, 0.0f, 0.0f, 0.0f);
		timePath.setPaint(transparent);
		return timePath;
	}

	public float getClosestValueToTime(TimePoint time) {
		Ratio timeDividedBySpacing = timeOfFirstRecord.until(time).length()
				.dividedBy(timeBetweenRecords);
		int index = timeDividedBySpacing.decimalValue(0, Rounding.HALF_UP)
				.intValueExact();
		return yValues.get(index);
	}

	// Public only for testing purposes. The offset this method provides is
	// intended to be used when
	// indexing into the yValues list.
	public int getOffsetIntoArray(TimePoint timeAtIndex) {
		if (timeAtIndex.isBefore(timeOfFirstRecord)) {
			throw new IllegalArgumentException(
					"requested time point must come after first time point");
		}
		Duration timeOffset = timeOfFirstRecord.until(timeAtIndex).length();
		Ratio indexOffset = timeOffset.dividedBy(timeBetweenRecords);
		return indexOffset.decimalValue(0, Rounding.HALF_UP).intValue();
	}

	public float getVerticalExageration() {
		return verticalExageration;
	}

	public String getUnits() {
		return units;
	}

	public void setVerticalExageration(float verticalExageration) {
		this.verticalExageration = verticalExageration;
	}

	public Paint getColor() {
		return color;
	}

	public void setColor(Paint color) {
		this.color = color;
	}
}
